Skip to content

fix(sdk): *Secure key helpers don't hash GetKeyResponse key material - #1066

Open
0xkurious wants to merge 1 commit into
Dstack-TEE:nextfrom
0xkurious:fix/js-sdk-viem-hex-and-secure-hash
Open

fix(sdk): *Secure key helpers don't hash GetKeyResponse key material#1066
0xkurious wants to merge 1 commit into
Dstack-TEE:nextfrom
0xkurious:fix/js-sdk-viem-hex-and-secure-hash

Conversation

@0xkurious

Copy link
Copy Markdown

Summary

toViemAccountSecure / toKeypairSecure (JS) and to_account_secure / to_keypair_secure (Python) all document:

This method applies SHA256 hashing to the complete key material for enhanced security.

But only the legacy GetTlsKeyResponse branch actually does this. The GetKeyResponse branch — i.e. client.getKey() followed by the *Secure helper, which is exactly the pattern the README recommends — falls straight through to the raw derived key, producing output byte-identical to the deprecated non-secure variant (toViemAccount / toKeypair / to_account / to_keypair).

So the documented security guarantee silently does not apply to the primary, documented usage path. Found this while wiring a TEE-attested signer against a live CVM (deriving an EVM address from getKey() to bind into a quote's REPORTDATA) and noticing toViemAccountSecure(key) produced the exact same address as the deprecated helper.

Root cause

This looks like a regression against the intent of #197, which introduced the *Secure family specifically to apply SHA256 across "complete key material" for both response types. The GetTlsKeyResponse branch got the hash; the GetKeyResponse branch didn't.

// sdk/js/src/viem.ts — toViemAccountSecure, before this PR
export function toViemAccountSecure(keyResponse: GetKeyResponse | GetTlsKeyResponse) {
  if (keyResponse.__name__ === 'GetTlsKeyResponse') {
    const hex = bytesToHex(sha256(keyResponse.asUint8Array())) // ✅ hashed
    return privateKeyToAccount(`0x${hex}`)
  }
  const hex = Array.from(keyResponse.key).map(b => b.toString(16).padStart(2, '0')).join('')
  return privateKeyToAccount(`0x${hex}`) // ❌ raw key, not hashed
}

Same pattern in sdk/js/src/solana.ts, sdk/python/src/dstack_sdk/ethereum.py, and sdk/python/src/dstack_sdk/solana.py.

Fix

Apply sha256() to keyResponse.key (JS) / get_key_response.decode_key() (Python) in the GetKeyResponse branch of all four *Secure functions, matching the existing GetTlsKeyResponse branch and the documented behavior.

⚠️ Breaking change

Anyone currently calling toViemAccountSecure / toKeypairSecure / to_account_secure / to_keypair_secure with a GetKeyResponse (the common case — not the deprecated TLS-key path) will get a different address/keypair after this fix, since the key material is now actually hashed as documented. The underlying getKey() derivation and the raw key itself are unaffected — only what these four wrapper functions do with it changes.

I don't have visibility into how many consumers rely on the current (unhashed) behavior, so I'm flagging this explicitly rather than deciding on versioning myself. Options I can see:

  1. Ship as a patch with a prominent changelog entry + migration note (fastest fix, correctness-critical for a function named "Secure").
  2. Bump minor/major per your semver policy for behavior changes in security-labeled APIs.
  3. If backward compatibility for GetKeyResponse callers matters more than closing the gap, an alternative is to fix the docstring instead of the code (state plainly that only the TLS path is hashed) — happy to switch the PR to that if preferred.

I went with fixing the implementation because a function literally named *Secure silently not doing what its docstring promises seems like the worse failure mode, but the versioning/migration call is yours.

Testing

Added a regression test to each of the four modules asserting the *Secure variant produces a different address/pubkey than the legacy variant for the same GetKeyResponse — this is exactly the assertion that would have caught this bug originally. All pre-existing tests still pass (verified locally against the phala simulator); a handful of unrelated pre-existing failures in sdk/js/src/__tests__/index.test.ts (secp256k1_prehashed validation) reproduce identically on unmodified main and are not touched by this PR.

  • sdk/js: npx vitest run src/__tests__/viem.test.ts src/__tests__/solana.test.ts → 14/14 passed
  • sdk/python: pytest tests/test_ethereum.py tests/test_solana.py → 14/14 passed

toViemAccountSecure, toKeypairSecure (JS) and to_account_secure,
to_keypair_secure (Python) all document "applies SHA256 hashing to
the complete key material for enhanced security" -- but only the
legacy GetTlsKeyResponse branch actually did this. The GetKeyResponse
branch (the path client.getKey() + these helpers, i.e. exactly what
the README recommends) fell straight through to the raw derived key,
identical to the deprecated non-secure variant.

Found while wiring a TEE-attested signer against a live CVM: the
security guarantee these functions are named and documented for
silently did not apply to the primary documented usage.

BREAKING: anyone currently using toViemAccountSecure/toKeypairSecure/
to_account_secure/to_keypair_secure with a GetKeyResponse (not
GetTlsKeyResponse) will get a different address/keypair after this
fix, since the key material is now actually hashed as documented.
Flagging for maintainers to decide on versioning/changelog treatment.

Added regression tests in all four modules asserting the *Secure
variant produces a different address/pubkey than the legacy variant
for the same GetKeyResponse -- this is exactly the assertion that
would have caught the bug.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant